<?php

require_once("ftp.exceptions.php");

class ftp {
    private $hostname          = '';
    private $username          = '';
    private $password          = '';
    private $port              = 21;
    private $timeout           = 20;
    private $connection        = null;
    private $root_directory    = '/';
    private $current_directory = '/';
    private $passive_mode      = true;
#    private $regular_expr      = "/(a|d|s|c)([r\-][w\-][x\-]){3}([\s]{1,})([\d])([\s]{1,})([\w]+)([\s]{1,})([\w]+)([\s]{1,})([\d]+)([\s]{1,})([\w]+)(\s)([\d]+)(\s)([\d]+):([\d]+)(\s)([^\s]+)/";
    private $regular_expr      = "/(a|s|d|c|\-)([r\-][w\-][x\-]){3}([\s]){1,}([\d]+)(\s)(\d){1,}([\s]+)(\d){1,}([\s]+)(\d){1,}([\s]+)([\w]+)([\s]){1,}([\d]+)([\s]+)([\d]+):([\d]+)([\s]+)([\w.]+)/";

    public function __construct($hostname, $username, $password, $port = 21, $timeout = 20)
    {
        $this->hostname = $hostname;
        $this->username = $username;
        $this->password = $password;
        $this->port     = isset($port) ? intval($port) : 21;
        $this->timeout  = isset($timeout) ? intval($timeout) : 20;
    }


    public function connect()
    {
        if (($this->connection = @ftp_connect($this->hostname, $this->port, $this->timeout)) === false) {
            throw new FtpConnectException("Connect to host {$this->hostname} failure");
        } else {
            if ((@ftp_login($this->connection, $this->username, $this->password)) === false) {
                throw new FtpAuthException("Login to host {$this->hostname} failure");
            }
            $this->work_directory    = ftp_pwd($this->connection);
            $this->current_directory = $this->work_directory;
        }
    }


    public function close()
    {
        if ($this->conn) {
            if (is_resource($this->conn)) {
                ftp_close($this->conn);
            }
            $this->conn =  null;
        }
    }


    public function set_passive_mode($passive_mode = true)
    {
        if ((@ftp_pasv($this->connection, $passive_mode)) === false) {
            throw new FtpOperationFailureException("Set ftp passive mode failure");
        }
    }


    public function get_current_directory()
    {
        $this->current_directory = ftp_pwd($this->connection);
        if ($this->current_directory === false) {
            throw new FtpOperationFailureException("Get current directoy failure");
        } else {
            return $this->current_directory;
        }
    }


    public function create_directory($directory, $mode = 0)
    {
        if (empty($directory)) {
            throw new FtpArgumentsException("Argument directory is invalid");
        } else {
            $this->create_directories(array($directory), $mode);
        }
    }


    public function create_directories($directories = array(), $mode = 0)
    {
        if (empty($directories)) {
            throw new FtpArgumentsException("Argument directories is invalid");
        } else {
            $i = 0; $j = 0; $nsegment = 0;
            $path_segments = array();
            $n = count($directories);
            for ($i = 0; $i < $n; ++ $i) {
                if ($directories[$i][0] !== '/') {
                    $directories[$i] = trim($this->current_directory, '/') . '/' . $directories[$i];
                } else {
                    $directories[$i] = trim($directories[$i], '/');
                }
                $path_segments[] = explode('/', $directories[$i]);
            }

            for ($i = 0; $i < $n; ++ $i) {
                ftp_chdir($this->connection, $this->root_directory);
                $nsegment = count($path_segments[$i]);
                for ($j = 0; $j < $nsegment; ++ $j) {
                    if ((@ftp_chdir($this->connection, $path_segments[$i][$j])) === false) {
                        if ((@ftp_mkdir($this->connection, $path_segments[$i][$j])) === false) {
                            throw new FtpOperationFailureException("Create directory {$directories[$i]} failure");
                        } else {
                            ftp_chdir($this->connection, $path_segments[$i][$j]);
                            if ($mode) {
                                ftp_chmod($this->connection, $mode);
                            }
                        }
                    }
                }
            }
        }
    }


    public function change_directory($directory)
    {
        $ret = @ftp_chdir($this->connection, $directory);
        if ($ret) {
            if ($directory[0] !== '/') {
                $this->current_directory = trim($this->current_directory, '/') . '/' . $directory;
            } else {
                $this->current_directory = $directory;
            }
        } else {
            throw new FtpOperationFailureException("Change directory to {$this->current_directory} failure");
        }
    }


    public function list_file($directory, $recursive = false)
    {
        return $this->__list_file($directory, $recursive);
    }


    protected function __list_file($current_directory, $recursive = false)
    {
        $result       = array();
        $raw_file_arr = ftp_rawlist($this->connection, $current_directory);
        $nraw_file    = count($raw_file_arr);
        if ($nraw_file) {
            $i = 0;
            for ($i = 0; $i < $nraw_file; ++ $i) {
                $matches = array();
                if (preg_match($this->regular_expr, $raw_file_arr[$i], $matches)) {
                    $file_type = $matches[1];
                    $file_size = $matches[10];
                    if ($file_type === 'd') { 
                        $file_name = rtrim($current_directory, '/') . '/' . ltrim($matches[19], '/');
                    } else {
                        if ($current_directory[0] === '/') {
                            $file_name = '/' . $matches[19];
                        } else { 
                            $file_name = rtrim($current_directory, '/') . '/' . $matches[19];
                        }
                    }

                    $result[] = array('type' => $file_type, 'file' => $file_name, 'size' => $file_size);
                    if ($file_type == 'd' && $recursive === true) {
                        $files = $this->__list_file(rtrim($file_name, '/'), $recursive);
                        if ($files) { 
                            $result = array_merge($result, $files);
                        }
                        unset($files);
                    }
                }
                unset($matches);
            }
        }
        return $result;
    }


    public function upload($file, $destination)
    {
        if (!file_exists($file)) {
            throw new FtpUploadException("Upload file {$file} not exists");
        }

        clearstatcache();
        if (empty($destination) || $destination === '/') {
            throw new FtpArgumentsException("Argument destination is invalid");
        }

        if ($destination[0] !== '/') {
            $destination = trim($this->current_directory, '/') . '/' . trim($destination, '/');
        } else {
            $destination = trim($destination, '/');
        }

        $segments = explode("/", $destination);
        $nsegment = count($segments);
        ftp_chdir($this->connection, $this->work_directory);
        if ($nsegment > 1) {
            for ($i = 0; $i < $nsegment; ++ $i) {
                if (@ftp_chdir($this->connection, $segments[$i])) {
                    if (!@ftp_mkdir($this->connection, $segments[$i])) {
                        throw new FtpOperationFailureException("Create directory {$destination} failure");
                    } else {
                        ftp_chdir($this->connection, $segments[$i]);
                    }
                }
            }
            $destination = ($segments[$nsegment - 1]) ? $segments[$nsegment - 1] : basename($file);
        }

        $ret = @ftp_nb_put($this->connection, $destination, $file, FTP_ASCII);
        while ($ret === FTP_MOREDATA) {
            $ret = ftp_nb_continue($this->connection);
        }

        if ($ret === FTP_FAILED) {
            throw new FtpUploadException("Upload file {$file} failure");
        }
    }


    public function download($file, $destination)
    {
        if (empty($file)) {
            throw new FtpArgumentsException("Argument file {$file} is invalid");
        }

        if (empty($destination)) {
           throw new FtpArgumentsException("Argument destination {$destination} is invalid");
        }

        if ($file[0] !== '/') {
            $file = trim($this->current_directory, '/') . '/' . $path;
        }

        $ret = ftp_nb_get($this->connection, $destination, $file, FTP_BINARY);
        while ($ret === FTP_MOREDATA) {
            $ret = ftp_nb_continue($this->connection);
        }

        if ($ret = FTP_FAILED) {
            throw new FtpDownloadException("Download file {$file} failure");
        }
    }


    public function remove_file($path)
    {
        if (empty($path)) {
            throw new FtpArgumentsException("Argument path {$path} is invalid");
        } else {
            $this->remove_dir($path);
        }
    }


    public function remove_dir($path)
    {
        $files = $this->__list_file($path, true);
        $nfile = count($files);
        for ($i = $nfile - 1; $i >= 0; -- $i) {
            if ($files[$i]['type'] === 'd') {
                ftp_rmdir($this->connection, $files[$i]['file']);
            } else {
                ftp_delete($this->connection, $files[$i]['file']);
            }
        }
        ftp_rmdir($this->connection, $path);
    }

    public function chmod($file, $mode)
    {
        if (empty($file)) {
             throw new FtpArgumentsException("Argument file is invalid");
        } else {
             $files = $this->__list_file($file, true);
             $nfile = count($files);
             for ($i = 0; $i < $nfile; ++ $i) {
                 ftp_chmod($this->connection, $mode, $files[$i]['file']);
             }
             ftp_chmod($this->connection, $mode, $file);
        }
    }
}